﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using ErrorDumper.PropertyExtracting;

namespace ErrorDumper {
    [Serializable]
    public class ExceptionPropertyCollection : IDictionary<string, string> {
        readonly Dictionary<string, string> _dict;
        static readonly IEqualityComparer<string> _comparer = StringComparer.OrdinalIgnoreCase;

        public IEqualityComparer<string> NameComparer { get { return _comparer; } }

        public ExceptionPropertyCollection() {
            _dict = new Dictionary<string, string>(_comparer);
        }

        public ExceptionPropertyCollection(IDictionary<string, string> properties) {
            if (properties == null)
                _dict = new Dictionary<string, string>(_comparer);
            else {
                _dict = new Dictionary<string, string>(properties.Count, _comparer);
                AddRange(properties);
            }
        }

        public ExceptionPropertyCollection(IDictionary<string, object> properties) {
            if (properties == null)
                _dict = new Dictionary<string, string>(_comparer);
            else {
                _dict = new Dictionary<string, string>(properties.Count, _comparer);
                AddRange(properties);
            }
        }

        public ExceptionPropertyCollection(object properties) {
            if (properties == null)
                _dict = new Dictionary<string, string>(_comparer);
            else {
                var props = TypeDescriptor.GetProperties(properties);
                _dict = new Dictionary<string, string>(props.Count, _comparer);
                foreach (PropertyDescriptor prop in props) {
                    object value;
                    try {
                        value = prop.GetValue(properties);
                    }
                    catch {
                        value = null;
                    }
                    Add(prop.Name, value);
                }
            }
        }

        static string ConvertToInvariantString(object value) {
            if (value == null)
                return null;

            if (value is string)
                return (string)value;

            if (value is DateTime)
                return ((DateTime)value).ToString("s");

            var converter = TypeDescriptor.GetConverter(value);
            if (converter == null)
                return null;

            try {
                return converter.ConvertToInvariantString(value);
            }
            catch {
                try { return value.ToString(); }
                catch { return null; }
            }
        }

        public void Add(string name, string value) {
            name = !string.IsNullOrWhiteSpace(name) ? name.Trim() : ExceptionProperty.Empty;
            _dict[name] = string.IsNullOrEmpty(value) ? string.Empty : value.Trim();
        }

        public void Add(string name, object value) {
            Add(name, ConvertToInvariantString(value));
        }

        public void Add(object name, object value) {
            Add(ConvertToInvariantString(name), ConvertToInvariantString(value));
        }

        public void Add(KeyValuePair<string, string> property) {
            Add(property.Key, property.Value);
        }

        public void Add(KeyValuePair<string, object> property) {
            Add(property.Key, property.Value);
        }

        public void Add(KeyValuePair<object, object> property) {
            Add(property.Key, property.Value);
        }

        public void Add(DictionaryEntry property) {
            Add(property.Key, property.Value);
        }

        public void AddRange(IDictionary<string, string> properties) {
            if (properties != null)
                foreach (var property in properties)
                    Add(property.Key, property.Value);
        }

        public void AddRange(IDictionary<string, object> properties) {
            if (properties != null)
                foreach (var property in properties)
                    Add(property.Key, property.Value);
        }

        public void AddRange(IDictionary<object, object> properties) {
            if (properties != null)
                foreach (var property in properties)
                    Add(property.Key, property.Value);
        }

        public void AddRange(IDictionary properties) {
            if (properties != null)
                foreach (DictionaryEntry property in properties)
                    Add(property.Key, property.Value);
        }

        public void AddRange(object properties) {
            if (properties != null)
                foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(properties)) {
                    object value;
                    try {
                        value = prop.GetValue(properties);
                    }
                    catch {
                        value = null;
                    }
                    Add(prop.Name, value);
                }
        }

        bool IDictionary<string, string>.ContainsKey(string key) {
            return _dict.ContainsKey(key);
        }

        public bool Contains(string name) {
            return _dict.ContainsKey(name);
        }

        ICollection<string> IDictionary<string, string>.Keys {
            get { return _dict.Keys; }
        }

        public ICollection<string> Names {
            get { return _dict.Keys; }
        }

        public bool Remove(string name) {
            return _dict.Remove(name);
        }

        public bool TryGetValue(string name, out string value) {
            return _dict.TryGetValue(name, out value);
        }

        public ICollection<string> Values {
            get { return _dict.Values; }
        }

        public string this[string name] {
            get {
                string value;
                _dict.TryGetValue(name, out value);
                return value;
            }
            set {
                Add(name, value);
            }
        }

        void ICollection<KeyValuePair<string, string>>.Add(KeyValuePair<string, string> item) {
            ((ICollection<KeyValuePair<string, string>>)_dict).Add(item);
        }

        public void Clear() {
            _dict.Clear();
        }

        bool ICollection<KeyValuePair<string, string>>.Contains(KeyValuePair<string, string> item) {
            return ((ICollection<KeyValuePair<string, string>>)_dict).Contains(item);
        }

        void ICollection<KeyValuePair<string, string>>.CopyTo(KeyValuePair<string, string>[] array, int arrayIndex) {
            ((ICollection<KeyValuePair<string, string>>)_dict).CopyTo(array, arrayIndex);
        }

        public int Count {
            get { return _dict.Count; }
        }

        bool ICollection<KeyValuePair<string, string>>.IsReadOnly {
            get { return ((ICollection<KeyValuePair<string, string>>)_dict).IsReadOnly; }
        }

        bool ICollection<KeyValuePair<string, string>>.Remove(KeyValuePair<string, string> item) {
            return ((ICollection<KeyValuePair<string, string>>)_dict).Remove(item);
        }

        public IEnumerator<KeyValuePair<string, string>> GetEnumerator() {
            return _dict.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator() {
            return _dict.GetEnumerator();
        }

        public static ExceptionPropertyCollection Copy(ExceptionPropertyCollection input, ExceptionPropertyCollection output) {
            if (output == null)
                return input;

            if (input == null)
                return null;

            foreach (var property in input)
                output.Add(property.Key, property.Value);
            return output;
        }

        public static void GetFromMetadata(object instance, ExceptionPropertyCollection properties, bool @override) {
            if (instance == null)
                throw new ArgumentNullException("instance");
            if (properties == null)
                throw new ArgumentNullException("properties");

            var type = instance.GetType();
            var attrType = typeof(ExceptionPropertyAttribute);

            foreach (var member in type.GetMembers(
                BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic |
                BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.InvokeMethod)) {

                var attrs = member.GetCustomAttributes(attrType, true);
                if (attrs.Length == 0)
                    continue;

                object value = null;
                var resolved = false;

                foreach (ExceptionPropertyAttribute attr in attrs) {
                    var key = !string.IsNullOrWhiteSpace(attr.ExceptionPropertyName)
                        ? attr.ExceptionPropertyName : member.Name;

                    if (@override || !properties.Contains(key)) {
                        if (!resolved) {
                            try {
                                switch (member.MemberType) {
                                    case MemberTypes.Field:
                                        value = ((FieldInfo)member).GetValue(instance);
                                        break;
                                    case MemberTypes.Method:
                                        value = ((MethodInfo)member).Invoke(instance, null);
                                        break;
                                    case MemberTypes.Property:
                                        value = ((PropertyInfo)member).GetValue(instance, null);
                                        break;
                                }
                                resolved = true;
                            }
                            catch {
                                value = null;
                            }
                        }

                        properties.Add(key, value);
                    }
                }
            }
        }

        public static void Build(object instance, ExceptionPropertyCollection properties, bool @override) {
            if (instance == null)
                throw new ArgumentNullException("instance");
            if (properties == null)
                throw new ArgumentNullException("properties");

            var accessor = instance as IExceptionPropertyAccessor;
            if (accessor != null)
                accessor.GetProperties(ref properties, @override);
            else
                GetFromMetadata(instance, properties, @override);

            ExceptionPropertyExtractors.ExtractProperties(instance, properties);

            var exception = instance as Exception;
            if (exception != null && exception.InnerException != null)
                Build(exception.InnerException, properties, false);
        }
    }
}